package view;

import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.Point;
import java.awt.image.BufferedImage;
import java.util.HashMap;
import java.util.Map;

import model.Model;
import model.entity.BasicEntity;
import model.entity.Mountable;
import model.map.Direction;
import model.map.FogOfWar;
import model.map.GameMap;
import model.map.Location;
import model.map.NoLimitHexLocationProjection;
import model.map.World;
import model.skill.Projectile;

public class MapOverlay extends Overlay {

	//TODO implement fog of war stuff
	
	private final int tileWidth = 52;//in pixels
	private final int tileHeight = 46;
	private final int tileQuarter = 13;//horizontal distance form the left most vertex, to the next vertex
	private final String semitransparentTileModelName = "semitransparentTile";
	
	private GameMap map;
	private Location center;
	private NoLimitHexLocationProjection proj;
	private Map<Location, BufferedImage> locationToImages;
	//private FogOfWar fow;
	
	public MapOverlay( Dimension dimension, GameMap map, Location center ) {
		super( dimension );
		this.map = map;
		this.center = center;
		locationToImages = new HashMap<Location, BufferedImage>();
		this.proj = new NoLimitHexLocationProjection();
	}
	
	public void setMap( GameMap map ) {
		this.map = map;
	}
	
	public void onResize() {} //this is a hook method (primitive) which this class doesn't use
	
	public void setCenterLocation(Location center ) {
		this.center = center;
	}
	
	public Location getCenterLocation() {
		return this.center;
	}
	
	private void updateEntityOnScreen() {
	} //TODO implement: this method updates the map
	//from entities (displayed on screen) to their (pixel) location. This is used by things as
	//stat overlay that has to draw on top of entities
	
	public void drawOverlay(Graphics g ) {
		//first draw the whole background black.
		g.fillRect( 0, 0, super.getOuterDimension().width, super.getOuterDimension().height);
		Location currentTile = new Location( center.getX(), center.getY() );
		Point currentScreenLocation = getUpperLeftFromCenter( new Point(super.getOuterDimension().width/2,
				super.getOuterDimension().height/2) );//start at the center and point at 
													  //the upper left point of center tile
		this.setToUpperLeftMostDrawingLocation( currentScreenLocation, currentTile );//this moves the
		//Location and Point (which are used for drawing) to the proper place (upper left part of screen)
		EntityOnScreen.clearEntitiesOnScreen();//clear the previous entities on screen
		paintScreenStartingFrom( currentScreenLocation, currentTile, g );
	}
	
	private void paintScreenStartingFrom( Point screenLocation, Location correspondingTile, Graphics g ) {
		boolean advanceSouthEast = true;
		while( screenLocation.y < this.getOuterDimension().height + this.tileHeight ) {
			//if the end of the screen has not been reached, paint current row and move down by 1
			paintRow( screenLocation, correspondingTile, g );
			if( advanceSouthEast ) {
				screenLocation = this.jumpSouthEast( screenLocation );
				correspondingTile = proj.projectLocation( correspondingTile, Direction.SE );
			} else {//advance SW otherwise
				screenLocation = this.jumpSouthWest( screenLocation );
				correspondingTile = proj.projectLocation( correspondingTile, Direction.SW );
			}
			advanceSouthEast = !( advanceSouthEast );//next time go the opposite way
		}
	}
	
	private void paintRow( Point screenLocation, Location correspondingTile, Graphics g ) {
		//paint an individual row going towards the right of the screen
		while( screenLocation.x < super.getOuterDimension().width ) {//while the end of the screen has not been reached
			paintTile( screenLocation, correspondingTile, g );//paint the tile
			screenLocation = this.jumpEast( screenLocation );//update the pixel location
			correspondingTile = new Location( correspondingTile.getX()-1 , correspondingTile.getY()+1 );
		}//the row has been painted
	}
	
	private void paintTile( Point screenLocation, Location correspondingTile, Graphics g ) {
		if( locationIsVisible(correspondingTile) ){
			//draw the whole tile as a single image (size = tileWidth x tileHeight) so that it can
			//stored for later use (by shaded locations)
			BufferedImage copy = new BufferedImage( tileWidth, tileHeight, BufferedImage.TYPE_INT_ARGB );
			Graphics cg = copy.getGraphics();
			drawTerrains( correspondingTile, cg );
			drawObstacles( correspondingTile, cg );
			drawDecal( correspondingTile, cg );
			drawAreaEffects( correspondingTile, cg );
			drawItems( correspondingTile, cg );
			drawVehicle( correspondingTile, cg );
			drawEntity( screenLocation, correspondingTile, cg );			
			drawProjectile( correspondingTile, cg );
			//save the location fr later use, as explained above ^
			this.locationToImages.put( correspondingTile, copy );
			//now draw the actual tile!
			g.drawImage( copy, screenLocation.x, screenLocation.y, null );
		} else if( locationIsShaded( correspondingTile ) ) {
			BufferedImage copy = this.locationToImages.get( correspondingTile );
			Image semitransparent = ModelToImage.getImage( this.semitransparentTileModelName );
			g.drawImage( copy, screenLocation.x, screenLocation.y, null );
			g.drawImage( semitransparent, screenLocation.x, screenLocation.y, null);
			
		} else {}//do not draw anything! It should be black.
	}
	
	private boolean locationIsVisible( Location l ) {
		FogOfWar fow = this.map.getFogOfWar();
		return fow.isVisisble( l );
	}
	
	private boolean locationIsShaded( Location l ) {
		FogOfWar fow = this.map.getFogOfWar();
		return fow.isShaded( l );
	}
	
	private void drawTerrains( Location correspondingTile, Graphics g ) {
		Model m = this.map.getTerrain( correspondingTile );//get the terrain
		if( m == null ) { return ; }
		drawHelper( correspondingTile, g, m );
	}
	
	private void drawObstacles( Location correspondingTile, Graphics g ) {
		Model m = this.map.getObstacle( correspondingTile );//get the obstacle
		if( m == null ) { return ; }
		drawHelper( correspondingTile, g, m );
	}
	
	private void drawDecal( Location correspondingTile, Graphics g ) {
		Model m = this.map.getDecal( correspondingTile );//get the decal
		if( m == null ) { return ; }
		drawHelper( correspondingTile, g, m );
	}
	
	private void drawItems( Location correspondingTile, Graphics g ) {
		Model m = this.map.getItem( correspondingTile );//get the item
		if( m == null ) { return ; }
		drawHelper( correspondingTile, g, m );
	}
	
	private void drawAreaEffects( Location correspondingTile, Graphics g ) {
		Model m = this.map.getAreaEffect( correspondingTile );//get the area effect
		if( m == null ) { return ; }
		drawHelper( correspondingTile, g, m );
	}
	
	private void drawVehicle( Location correspondingTile, Graphics g ) {
		Mountable m = this.map.getMountable( correspondingTile );//get the vehicle
		if( m == null ) { return ; }
		drawHelperDirectional( correspondingTile, g, m.getName(), m.getDirection() );
	}
	
	private void drawEntity( Point screenLocation, Location correspondingTile, Graphics g ) {
		BasicEntity m = this.map.getEntity( correspondingTile );//get the entity
		if( m == null) { return ; }
		else if (m != World.getInstance().getPlayer())
			EntityOnScreen.addEntity( m, screenLocation );
		drawHelperDirectional( correspondingTile, g, m.getName(), m.getDirection());
	}
	
	private void drawProjectile( Location correspondingTile, Graphics g ) {
		Projectile m = this.map.getProjectile( correspondingTile );//get the projectile
		if( m == null ) { return ; }
		drawHelper( correspondingTile, g, m );
	}
	
	private void drawHelper( Location correspondingTile, Graphics g,
			Model m) {
		if ( m == null ) { return ; }//nothing to draw 
		Image img = ModelToImage.getImage( m.getName() );//get the associated image
		g.drawImage( img,//draw the image (with scaling)
			0, 0, tileWidth, tileHeight,
			0, 0, img.getWidth(null), img.getHeight(null), null );
	}
	
	private void drawHelperDirectional( Location correspondingTile,
			Graphics g, String modelName, Direction d) {
		if ( modelName == null ) { return ; }//nothing to draw 
		Image img = DirectionalModelToImage.getImage( modelName, d );//get associated img
		g.drawImage( img,//draw the image (with scaling)
			0, 0, tileWidth, tileHeight,
			0, 0, img.getWidth(null), img.getHeight(null), null );
	}
	
	
	/**
	 * Given the center point of a tile in pixels, get the upper left corner
	 * @param centerPoint center point in pixels
	 * @return upper left of tile in pixels
	 */
	private Point getUpperLeftFromCenter( Point centerPoint ) {
		return new Point( centerPoint.x - (tileWidth/2), centerPoint.y - (tileHeight/2) );
	}
	
	/**
	 * Sets the Point to the upper left most pixels that should be drawn (could be off-screen) and 
	 * changes the Location to the corresponding tile (could be non-existent or negative).
	 * @param screenPixelCoordinate
	 * @param tile
	 */
	private void setToUpperLeftMostDrawingLocation( Point screenPixelCoordinate, Location tile ) {
		while( screenPixelCoordinate.y > 0 ) {//while it is not off-screen, keep going up
			Point up = this.jumpNorth(screenPixelCoordinate);
			screenPixelCoordinate.setLocation( up.x, up.y );
			//apart form moving the coordinate on the screen, also move the tile up
			Location projection = this.proj.projectLocation( tile, Direction.N );
			tile.setLocation( projection.getX(), projection.getY() );
		}//by now the y-aspect of it is properly set, now move to the left of the screen
		while( screenPixelCoordinate.x > 0 ) {
			Point left = this.jumpWest(screenPixelCoordinate);
			screenPixelCoordinate.setLocation( left.x, left.y );
			//apart form moving the coordinate on the screen, also move the tile left
			tile.setLocation( tile.getX() + 1, tile.getY() - 1 );
		}
		//now the tile/screen location is at the upper left point. Have to move this 1 tile NW
		//because otherwise the edge of the screen might be ragged
		Point nw = this.jumpNorthWest( screenPixelCoordinate );
		screenPixelCoordinate.setLocation( nw.x, nw.y );
		Location projection = this.proj.projectLocation( tile, Direction.NW );
		tile.setLocation( projection.getX(), projection.getY() );
	}
	
	/**
	 * Given the upper left point of a tile (in pixels), returns the upper left point of the tile
	 * in the E direction. 
	 * @param startingPoint starting point.
	 * @return 
	 */
	private Point jumpEast( Point startingPoint ) {
		return new Point( (int) ((double) startingPoint.x + (double) tileWidth * (6.0/4.0)),
				startingPoint.y );
	}
	
	/**
	 * Given the upper left point of a tile (in pixels), returns the upper left point of the tile
	 * in the W direction. 
	 * @param startingPoint starting point.
	 * @return 
	 */
	private Point jumpWest( Point startingPoint ) {
		return new Point( (int) ((double) startingPoint.x - (double) tileWidth * (6.0/4.0)),
				startingPoint.y );
	}
	
	/**
	 * Given the upper left point of a tile (in pixels), returns the upper left point of the tile
	 * in the N direction. 
	 * @param startingPoint starting point.
	 * @return 
	 */
	private Point jumpNorth( Point startingPoint ) {
		return new Point( startingPoint.x ,
				startingPoint.y - tileHeight );
	}
	
	/**
	 * Given the upper left point of a tile (in pixels), returns the upper left point of the tile
	 * in the S direction. 
	 * @param startingPoint starting point.
	 * @return 
	 */
	private Point jumpSouth( Point startingPoint ) {
		return new Point( startingPoint.x ,
				startingPoint.y + tileHeight );
	}
	
	/**
	 * Given the upper left point of a tile (in pixels), returns the upper left point of the tile
	 * in the NW direction. 
	 * @param startingPoint starting point.
	 * @return 
	 */
	private Point jumpNorthWest( Point startingPoint ) {
		return new Point( (int) ((double) startingPoint.x - (double) tileWidth * (3.0/4.0)),
				(int) (startingPoint.y - (double) tileHeight/2 ) );
	}
	
	/**
	 * Given the upper left point of a tile (in pixels), returns the upper left point of the tile
	 * in the NE direction. 
	 * @param startingPoint starting point.
	 * @return 
	 */
	private Point jumpNorthEast( Point startingPoint ) {
		return new Point( (int) ((double) startingPoint.x + (double) tileWidth * (3.0/4.0)),
				(int) (startingPoint.y - (double) tileHeight/2 ) );
	}
	
	/**
	 * Given the upper left point of a tile (in pixels), returns the upper left point of the tile
	 * in the SW direction. 
	 * @param startingPoint starting point.
	 * @return 
	 */
	private Point jumpSouthWest( Point startingPoint ) {
		return new Point( (int) ((double) startingPoint.x - (double) tileWidth * (3.0/4.0)),
				(int) (startingPoint.y + (double) tileHeight/2 ) );
	}
	
	/**
	 * Given the upper left point of a tile (in pixels), returns the upper left point of the tile
	 * in the SE direction. 
	 * @param startingPoint starting point.
	 * @return 
	 */
	private Point jumpSouthEast( Point startingPoint ) {
		return new Point( (int) ((double) startingPoint.x + (double) tileWidth * (3.0/4.0)),
				(int) (startingPoint.y + (double) tileHeight/2 ) );
	}

	@Override
	public void update(Model m) {
	}
}
